package sardine;

import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.multipart.Attribute;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import sardine.log.Logs;
import sardine.route.RouteMatched;
import sardine.utils.SardineUtils;
import sardine.utils.SardineUtils.RoutePathEntry;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @auth bruce_sha
 * @date 2015/5/21
 */
public class Request {

    private static final String X_Requested_With = "X-Requested-With";

    private FullHttpRequest request;

    private List<String> splats;
    private Set<String> headers;
    private Map<String, String> params;
    private QueryParamsMap queryMap;

    private String body;
    private byte[] bodyAsBytes;

    //TODO 暂不支持
    private Session session;

    //TODO
    protected Request() {
    }

    //TODO
    Request(RouteMatched match, FullHttpRequest request) {
        this.request = request;
        changeMatch(match);
    }

    //TODO
    protected void changeMatch(RouteMatched match) {
        //TODO:
//        List<String> requestList = SardineUtils.convertRouteToList(match.requestURI());
//        List<String> matchedList = SardineUtils.convertRouteToList(match.routeURI());

        List<RoutePathEntry> routePathEntries = SardineUtils.convertRouteToListEntry(match.requestURI(), match.routeURI());

        params = parseParams(routePathEntries);
        Logs.debug(() -> "parse params: " + params);
        splats = parseSplats(routePathEntries);
        Logs.debug(() -> "parse splats: " + splats);
    }

    public Map<String, String> params() {
        return Collections.unmodifiableMap(params);
    }

    public String params(String param) {
        if (param == null) return null;
        if (param.startsWith(":"))
            return params.get(param);
        else
            return params.get(":" + param);
    }

    //TODO
    public Optional<String> params1(String param) {

        return null;
    }

    //TODO
    public List<String> multiParams(String param) {
        return null;
    }

    public String[] splats() {
        return splats.toArray(new String[splats.size()]);
    }

    public String method() {
        return request.method().toString();
    }

    public boolean ajax() {
        return headers(X_Requested_With) != null;
    }

    /**
     * @return the scheme
     */
    public String scheme() {
//        return request.getScheme();
        //TODO:buru
        return "";
    }

    /**
     * @return the host
     */
    public String host() {
        return request.headers().getAndConvert(HttpHeaderNames.HOST);
//        return request.getHeader("host");
    }

    /**
     * @return the user-agent
     */
    public String userAgent() {
        return request.headers().getAndConvert(HttpHeaderNames.USER_AGENT);
//        return request.getHeader(USER_AGENT);
    }

    /**
     * @return the server port
     */
    public int port() {
        return Sardine.port;
//        return request.getServerPort();
    }


    /**
     * @return the path info
     * Example return: "/example/foo"
     */
    public String pathInfo() {
        return request.uri();
//        return request.getPathInfo();
    }

    /**
     * @return the servlet path
     */
    public String servletPath() {
        //TODO:buru
        return "";
//        return request.getServletPath();
    }

    /**
     * @return the context path
     */
    public String contextPath() {
        //TODO:buru
        return "";
//        return request.getContextPath();
    }

    /**
     * @return the URL string
     */
    public String url() {
        //TODO:buru
        return "";
//        return request.getRequestURL().toString();
    }

    /**
     * @return the content type of the body
     */
    public String contentType() {
        //TODO:buru
        return "";
//        return request.getContentType();
    }

    /**
     * @return the client's IP address
     */
    public String ip() {
        //TODO:buru
        return "";
//        return request.getRemoteAddr();
    }

    /**
     * @return the request body sent by the client
     */
    public String body() {
        if (body == null) readBody();
        return body;
    }

    public byte[] bodyAsBytes() {
        if (bodyAsBytes == null) readBody();
        return bodyAsBytes;
    }

    private void readBody() {

//        DefaultFullHttpRequest fullHttpRequest = (DefaultFullHttpRequest) request;

        try {
//			bodyAsBytes = IOUtils.toByteArray(request.getInputStream());
//			body = IOUtils.toString(new ByteArrayInputStream(bodyAsBytes));


            body = request.content().toString(StandardCharsets.UTF_8);
            bodyAsBytes = request.content().array();//body.getBytes(Charsets.UTF_8);
        } catch (Exception e) {
            Logs.warn("Exception when reading body", e);
        }
    }

    /**
     * @return the length of request.body
     */
    public int contentLength() {
        return request.content().readableBytes();
//        return request.getContentLength();
    }

    /**
     * Gets the value for the provided header
     *
     * @param header the header
     * @return the value of the provided header
     */
    public String headers(CharSequence header) {
        return request.headers().getAndConvert(header);
//        return request.getHeader(header);
    }

    /**
     * @return all query parameters
     */
    public Set<String> queryParams() {

        Set<String> re = new HashSet<>();
        // query string
        final String uri = request.uri();
        final QueryStringDecoder query = new QueryStringDecoder(uri);

        re.addAll(query.parameters().keySet());

        // http body
        final HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(request);
        for (InterfaceHttpData httpData : decoder.getBodyHttpDatas()) {
            InterfaceHttpData.HttpDataType dt = httpData.getHttpDataType();
            if (dt == InterfaceHttpData.HttpDataType.Attribute) {
                try {
                    String name = httpData.getName();
                    re.add(name);
                    String value = ((Attribute) httpData).getValue();
                } catch (IOException e) {
                    // nothing
                }
            }
        }
        return re;
//        return request.getParameterMap().keySet();
    }

    /**
     * gets the query param
     *
     * @param queryParam the query parameter
     * @return the value of the provided queryParam
     * Example: query parameter 'id' from the following request URI: /hello?id=foo
     */
    public String queryParams(String queryParam) {

//        List<String> re = Lists.newArrayList();

        // http body
        final HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(request);
        for (InterfaceHttpData httpData : decoder.getBodyHttpDatas()) {
            InterfaceHttpData.HttpDataType dt = httpData.getHttpDataType();
            if (dt == InterfaceHttpData.HttpDataType.Attribute) {
                try {
                    if (queryParam.equals(httpData.getName()))
//                        re.add(((Attribute) httpData).getValue());
                        return ((Attribute) httpData).getValue();
                } catch (IOException e) {
                    // nothing
                }
            }
        }

//        if (!re.isEmpty()) return re.iterator().next();
//        Optional<String> first = re.stream().findFirst();
//        if (first.isPresent()) return first.singleton();

        // query string
        final String uri = request.uri();
        final QueryStringDecoder query = new QueryStringDecoder(uri);
        final Map<String, List<String>> params = query.parameters();
        List<String> values = params.getOrDefault(queryParam, Collections.emptyList());
        return values.stream().findFirst().orElse(null);

//        Iterator<String> it = params.getOrDefault(queryParam, Collections.emptyList()).iterator();
//        return it.hasNext() ? it.next() : null;
//        return request.getParameter(queryParam);
    }

    /**
     * @return all headers
     */
    public Set<String> headers() {
//        request.headers()
        return request.headers().namesAndConvert(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return 0;
            }
        });

//        if (headers == null) {
//            headers = new TreeSet<String>();
//
//
//            Enumeration<String> enumeration = request.getHeaderNames();
//            while (enumeration.hasMoreElements()) {
//                headers.add(enumeration.nextElement());
//            }
//        }
//        return headers;
    }

    /**
     * @return the query string
     */
    public String queryString() {
        //TODO:buru
        final String uri = request.uri();
        final QueryStringDecoder query = new QueryStringDecoder(uri);
        return query.toString();
//        return request.getQueryString();
//        return request.getQueryString();
    }

    /**
     * Sets an attribute on the request (can be fetched in filters/routes later in the chain)
     *
     * @param attribute The attribute
     * @param value     The attribute value
     */
    public void attribute(String attribute, Object value) {
        //TODO:buru
        throw new RuntimeException();
//        request.setAttribute(attribute, value);
    }

    /**
     * Gets the value of the provided attribute
     *
     * @param attribute The attribute value or null if not present
     * @return the value for the provided attribute
     */
    public Object attribute(String attribute) {
        //TODO:buru
        throw new RuntimeException();
//        return request.getAttribute(attribute);
    }


    /**
     * @return all attributes
     */
    public Set<String> attributes() {

        //TODO:buru
        throw new RuntimeException();

//        Set<String> attrList = new HashSet<String>();
//        Enumeration<String> attributes = (Enumeration<String>) request.getAttributeNames();
//        while (attributes.hasMoreElements()) {
//            attrList.add(attributes.nextElement());
//        }
//        return attrList;
    }

    public FullHttpRequest raw() {
        return request;
    }

    /**
     * @return the query map
     */
    public QueryParamsMap queryMap() {
        initQueryMap();

        return queryMap;
    }

    /**
     * @param key the key
     * @return the query map
     */
    public QueryParamsMap queryMap(String key) {
        return queryMap().get(key);
    }

    private void initQueryMap() {
        //TODO:buru
        throw new RuntimeException();

//        if (queryMap == null) {
//            queryMap = new QueryParamsMap(raw());
//        }
    }

    /**
     * Returns the current session associated with this request,
     * or if the request does not have a session, creates one.
     *
     * @return the session associated with this request
     */
    public Session session() {
        //TODO:buru
        throw new RuntimeException();
//        if (session == null) {
//            session = new Session(request.getSession());
//        }
//        return session;
    }

    /**
     * Returns the current session associated with this request, or if there is
     * no current session and <code>create</code> is true, returns  a new session.
     *
     * @param create <code>true</code> to create a new session for this request if necessary;
     *               <code>false</code> to return null if there's no current session
     * @return the session associated with this request or <code>null</code> if
     * <code>create</code> is <code>false</code> and the request has no valid session
     */
    public Session session(boolean create) {
//        if (session == null) {
//            HttpSession httpSession = request.getSession(create);
//            if (httpSession != null) {
//                session = new Session(httpSession);
//            }
//        }
//        return session;
        //TODO:buru
        throw new RuntimeException();
    }

    /**
     * @return request cookies (or empty Map if cookies dosn't present)
     */
    public Map<String, String> cookies() {

        String value = request.headers().getAndConvert(HttpHeaderNames.COOKIE);

        if (value == null) return new HashMap<>();

        Set<Cookie> cookies = ServerCookieDecoder.decode(value);

        return cookies.stream().collect(Collectors.toMap(Cookie::name, c -> c.value()));

//        final Map<String, String> re = Maps.newHashMap();
//
//        for (Cookie cookie : cookies) {
////                response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.encode(cookie));
//            re.PUT(cookie.name(), cookie.value());
//        }

//        if (!cookies.isEmpty()) {
//            // Reset the cookies if necessary.
//            for (Cookie cookie : cookies) {
////                response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.encode(cookie));
//            }
//        }

//        for (Cookie cookie : cookies) {
//            responseContent.append("COOKIE: " + cookie + "\r\n");
//        }

//        Map<String, String> result = new HashMap<String, String>();
//        Cookie[] cookies = request.getCookies();
//        if (cookies != null) {
//            for (Cookie cookie : cookies) {
//                result.PUT(cookie.getName(), cookie.getValue());
//            }
//        }
//        return result;

//        //TODO:buru
//        throw new RuntimeException();
    }

    public static void main(String[] args) {
        Set<Cookie> cookies = new HashSet<>();
        cookies.add(new DefaultCookie("id", "123"));

        Optional<Cookie> cookie = cookies.stream().filter(c -> c.name().equalsIgnoreCase("id")).findFirst();

        if (cookie.isPresent())
            System.out.println(cookie.get());
        else
            System.out.println("none");
    }

    /**
     * Gets cookie by name.
     *
     * @param name name of the cookie
     * @return cookie value or null if the cookie was not found
     */
    public String cookie(String name) {

        String value = request.headers().getAndConvert(HttpHeaderNames.COOKIE);

        if (value == null) return null;

        Set<Cookie> cookies = ServerCookieDecoder.decode(value);

        Optional<Cookie> optionalCookie = cookies.stream().filter(c -> c.name().equals(name)).findFirst();

        if (!optionalCookie.isPresent()) return null;
        else return optionalCookie.get().value();

//        cookies.stream().collect(Collectors.toMap(e -> e.name(), Cookie::value));

//        Cookie[] cookies = request.getCookies();
//        if (cookies != null) {
//            for (Cookie cookie : cookies) {
//                if (cookie.getName().equals(name)) {
//                    return cookie.getValue();
//                }
//            }
//        }
//        return null;
//        throw new RuntimeException();
    }

    public String uri() {
        return request.uri();
    }

    public String protocol() {
        return request.protocolVersion().text().toString();
    }

    private static Map<String, String> parseParams(List<RoutePathEntry> routePathEntries) {
        return routePathEntries
                .parallelStream()
                .filter(e -> SardineUtils.notNull(e.requestPart()))
                .filter(e -> SardineUtils.notNull(e.routePart()))
                .filter(e -> SardineUtils.isParam(e.routePart()))
                .collect(Collectors.toMap(e -> e.routePart(), e -> e.requestPart()));


//        Map<String, String> params = new HashMap<String, String>();
//
//        for (int i = 0; (i < requestPath.size()) && (i < matchedPath.size()); i++) {
//            String matchedPart = matchedPath.singleton(i);
//            if (SardineUtils.isParam(matchedPart)) {
//                Logs.debug("matchedPart: "
//                        + matchedPart
//                        + " = "
//                        + requestPath.singleton(i));
//                params.PUT(matchedPart.toLowerCase(), requestPath.singleton(i));
//            }
//        }
//        return Collections.unmodifiableMap(params);
    }

    private static List<String> parseSplats(List<RoutePathEntry> routePathEntries) {

        return routePathEntries
                .parallelStream()
                .filter(e -> SardineUtils.notNull(e.requestPart()))
                .filter(e -> SardineUtils.notNull(e.routePart()))
                .filter(e -> SardineUtils.isSplat(e.routePart()))
                .map(e -> e.requestPart())
                .collect(Collectors.toList());
    }

    private static List<String> parseSplats(List<String> request, List<String> matched) {
        Logs.debug("parse splats");

        int nbrOfRequestParts = request.size();
        int nbrOfMatchedParts = matched.size();

        boolean sameLength = (nbrOfRequestParts == nbrOfMatchedParts);

        List<String> splat = new ArrayList<String>();

        for (int i = 0; (i < nbrOfRequestParts) && (i < nbrOfMatchedParts); i++) {
            String matchedPart = matched.get(i);

            if (SardineUtils.isSplat(matchedPart)) {

                StringBuilder splatParam = new StringBuilder(request.get(i));
                if (!sameLength && (i == (nbrOfMatchedParts - 1))) {
                    for (int j = i + 1; j < nbrOfRequestParts; j++) {
                        splatParam.append("/");
                        splatParam.append(request.get(j));
                    }
                }
                splat.add(splatParam.toString());
            }
        }
        return Collections.unmodifiableList(splat);
    }

    //TODO: 直接修改请求uri，其他值不变
    public void rewrite(String uri) {
        request.setUri(Objects.requireNonNull(uri));
    }

    public <T> T bodyExtract() {
        return null;
    }


    @Override
    public String toString() {
        return request.toString();
    }
}
